﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Caliburn.Micro;
using Google.GData.Client;
using Google.GData.Client.ResumableUpload;
using Google.GData.Extensions.MediaRss;
using Google.GData.YouTube;
using Google.YouTube;

namespace AppReviewerClient
{
    /*
     * requisiti.
     * devo avere progresso dentro ui e file corrente.
     * devo poter riprendere download e salvare lista corrente.
     * devo dare errori trovati.
     * mi basta fare un upload alla volta e voglio avere possibilita di stoppare.
     * 
     * Questa classe si occupa di fare upload video.
     * Lancia eccezzione o errori ( tra qui credenziali you tube non corrette)
     * - Si occupa di gestire la coda video.
     * 
     * - AddVideo
     * - PauseUpload
     * - StartUpload
     * 
     * - devo tenere conto della possibilita di più utenti su stesso computer.
     *  quindi faccio upload per l'utente corrente.
     *  
     * non si occupa della coda video.
     * solamente upload file.
     */

    public class YouTubeUploader : PropertyChangedBase
    {
        public bool AddVideo(string filepath)
        {
            if (_credentials == null)
            {
                RequestCredentials();
                return false;
            }

            /*
             * Check username
             */
            if (!Finished())
                return false;
            try
            {
                CurrentUploadItem = new UploadItem("Title") { Private = true, Filepath = filepath };

                StartUpload(new List<UploadItem>() { CurrentUploadItem });
            }
            catch (Exception)
            {

                return false;
                //Gestisci credenziali non corrette
            }
            return true;
        }

        private UploadItem _uploadItem;
        public UploadItem CurrentUploadItem
        {
            get { return _uploadItem; }
            set { _uploadItem = value; NotifyOfPropertyChange(() => _uploadItem); }
        }

        private static int MaxQueue = 1;
        private static int ChunkSize = 3;
        private static int AutomaticRetries = 4;

        private readonly Timer _retryTimer = new Timer();

        public YouTubeUploader()
        {
            _retryTimer.Interval = 6000;

            _retryTimer.Tick += RetryTimer_Tick;
        }

        private Authenticator youTubeAuthenticator;
        private const int COLUMN_MAX = 6;
        private const int COLUMNINDEX_FILENAME = 5;
        private const int COLUMNINDEX_STATUS = 6;
        private const int COLUMNINDEX_VIDEOID = 7;

        private const string CONFIG_DEVKEY = "DevKey";
        private const string CONFIG_CSVFILE = "CsvFile";
        private const string CONFIG_OUTPUTFILE = "OutputFile";
        private const string CONFIG_MAXTHREADS = "MaxThreads";
        private const string CONFIG_CHUNKSIZE = "ChunkSize";
        private const string CONFIG_RETRYCOUNT = "RetryCount";
        private const string CONFIG_USERNAME = "Username";
        private const string CONFIG_PASSWORD = "Password";
        private const string CONFIG_YTACCOUNT = "YoutubeAccount";

        private const string AutoStartString = "/autostart";
        private bool autoStart = false;
        private List<UserState> _queue = new List<UserState>();
        private readonly List<UserState> _retryQueue = new List<UserState>();

        private const string ApplicationName = "AppReviewer";

        // this is the default devkey that has very limited allowance against the service
        private const string DeveloperKeyDefault = "AI39si4Ye1PlPPsz1x0rQ64AlL0oanu1yh8FHdc_mWPSwn4jZyPMneAP6sldepx661VYw9oHvxXdESUKT7KGIZHpAQrWs5Cqbg";
        // this is a different devkey, uncomment this line and comment the above 
        //  private const string DeveloperKeyDefault = "AI39si5HV9zaLn4okq_gTqY4vARdZBf2_8D3bTkiK2FWgqaVVH9tITjBa6nyaAdg19Y4in-hnqcFTXu7i3d-2RIKSCvIMBARAg";

        private string developerKey;
        //private string csvFileName;
        //private string outputFileName;
        private string youtubeAccount;

        private GDataCredentials _credentials;
        private readonly Object _flag = new Object();

        // the applications is done when both queues are empty
        private bool Finished()
        {
            if (this._queue.Count == 0 &&
                this._retryQueue.Count == 0)
            {
                return true;
            }
            return false;
        }

        // we set the application title different if we still have pending uploads
        private void SetTitle()
        {
            //    if (NumberOfUploadsPending() > 0)
            //    {
            //        this.Text = YouTubeUploader.ApplicationName + " - " + NumberOfUploadsPending() + " uploads pending";
            //    }
            //    else
            //    {
            //        this.Text = YouTubeUploader.ApplicationName;
            //    }
        }

        // when the form loads, we want to load the application settings,
        // get the cmd line args, and process that 
        public void Login(string username, string password, string accountname)
        {
            LoadAppSettings();
            //  GetCmdLineArgs();

            if (this._credentials != null)
            {
                try
                {
                    this.youTubeAuthenticator = new ClientLoginAuthenticator(YouTubeUploader.ApplicationName,
                        ServiceNames.YouTube, this._credentials);
                    this.youTubeAuthenticator.DeveloperKey = this.developerKey;

                }
                catch
                {
                    this.youTubeAuthenticator = null;

                    RequestCredentials();
                }
            }

            if (this.youTubeAuthenticator == null)
            {
                this._credentials = new GDataCredentials(username, password);

                //GoogleClientLogin loginDialog = new GoogleClientLogin("cncsimple@gmail.com");

                //loginDialog.ShowDialog();

                this.youTubeAuthenticator = new ClientLoginAuthenticator(YouTubeUploader.ApplicationName,
                                                                         ServiceNames.YouTube, _credentials) { DeveloperKey = this.developerKey };


                this.youtubeAccount = accountname;

            }




            //if (this.csvFileName != null)
            //{
            //    //  LoadCSVFile(this.csvFileName);
            //    if (this.autoStart)
            //    {
            //        Upload_Click(null, null);
            //    }
            //}
        }

        //// helper to toggle the buttons (pending if we are processing uploads
        //// when only cancel is active)
        //private void ToggleButtons(bool enabled)
        //{
        //    this.OpenCSVFile.Enabled = enabled;
        //    this.Upload.Enabled = enabled;
        //    this.MaxQueue.Enabled = enabled;
        //    this.ChunkSize.Enabled = enabled;
        //    this.ChooseOutputFile.Enabled = enabled;

        //    EnableCancelButton(!enabled);

        //    this.Upload.Enabled = enabled && NumberOfUploadsPending() > 0;
        //}

        //// helper to make the cancel button visible and enabled (or not)
        //private void EnableCancelButton(bool value)
        //{
        //    this.CancelAsync.Enabled = value;
        //    this.CancelAsync.Visible = value;
        //}

        /// <summary>
        /// the number of pending uplods is the sum of the retry queue
        /// and the rows that have not been processed at all.
        /// </summary>
        /// <returns></returns>
        //private int NumberOfUploadsPending()
        //{
        //    int count = this.retryQueue.Count;

        //    // this will go over all rows and tries to upload them
        //    foreach (DataGridViewRow row in this.csvDisplayGrid.Rows)
        //    {
        //        if (row.Cells[COLUMNINDEX_STATUS].Tag == null)
        //            count++;
        //    }
        //    return count;
        //}

        // Upload button was clicked. Modify UI state and fill the processing queue
        // sarebbe meglio add upload

        private void StartUpload(List<UploadItem> uploadList)
        {
            // must be alredy logged in
            //   ToggleButtons(false);

            EnsureRU();

            Trace.TraceInformation("Uploading started");
            Trace.Indent();
            Trace.TraceInformation("Upload list contains " + uploadList.Count + " items");
            Trace.TraceInformation("Retry counter = " + AutomaticRetries);
            Trace.TraceInformation("Threads = " + MaxQueue);
            Trace.TraceInformation("Chunksize = " + ChunkSize);
            Trace.Unindent();

            // this will go over all rows and set the status
            foreach (var item in uploadList)
            {
                if (item.Status == null)
                {
                    item.Status = "Waiting to be uploaded....";
                }
            }

            // this will go over all rows and try to upload them
            foreach (var item in uploadList)
            {
                UploadRow(item);
            }

            RetryRows();

            if (Finished())
            {
                this.OnDone(null, null);
            }
        }

        // returns true if a row was added to the queue
        private bool UploadRow(UploadItem item)
        {
            if (this._queue.Count >= (int)MaxQueue)
            {
                return false;
            }

            var u = item.UserState;
            // if this was already processed, don't. It will be either
            // in the queue already or moved to the retry queue
            if (u != null)
            {
                return false;
            }

            return InsertVideo(item, 1);
        }

        
        /// <summary>
        /// Gets the current encode progress
        /// </summary>
        private double _uploadProgress;
        public double UploadProgress
        {
            get { return _uploadProgress; }
            set
            {
                _uploadProgress = value;
                NotifyOfPropertyChange(() => UploadProgress);
            }
        }
        // when we get a progress notification we remember a bunch of state info send
        // that info is needed to retry later
        private void OnProgress(object sender, AsyncOperationProgressEventArgs e)
        {
            var u = e.UserState as UserState;

            if (u != null && u.UploadItem != null)
            {
                // Aggiorno info necessarie per riprendere download 
                // salvo su file su cartella inuploading con estensione *.temp
                FilepathHelper.SavePartialDownloadInfo(u);

                string status = "";
                if (u.RetryCounter > 1)
                {
                    status = "Retrying (" + (u.RetryCounter - 1).ToString() + "), uploading: " + e.ProgressPercentage + "% done";
                }
                else
                {
                    status = "Uploading: " + e.ProgressPercentage + "% done";
                    UploadProgress = e.ProgressPercentage;
                }

                Trace.TraceInformation("OnProgress: " + status);
                Trace.Indent();
                Trace.TraceInformation("Verb: " + e.HttpVerb);
                Trace.TraceInformation("Uri: " + e.Uri);
                Trace.TraceInformation("Current position: " + e.Position);
                Trace.Unindent();

                u.CurrentPosition = e.Position;
                u.ResumeUri = e.Uri;
                u.HttpVerb = e.HttpVerb;
                u.UploadItem.Status = status;
            }
        }

        // send when an upload is done. This can mean it completed, it failed
        // or it was cancelled
        private void OnDone(object sender, AsyncOperationCompletedEventArgs e)
        {
            if (e != null)
            {
                var u = e.UserState as UserState;

                Trace.TraceInformation("OnDone - Upload finished for :" + u.ResumeUri);
                Trace.Indent();

                if (u != null && u.UploadItem != null)
                {
                    if (e.Cancelled)
                    {
                        Trace.TraceInformation("Cancelled. Current Pos = " + u.CurrentPosition);
                        u.UploadItem.Status = "Operation was cancelled";
                        // if it was cancelled, reset the retry counter
                        u.RetryCounter = 0;
                        u.UploadItem.UserState = u;
                        AddToRetryQueue(u);
                    }
                    else if (e.Error != null)
                    {
                        if (e.Error.Message == "Invalid credentials")
                        {
                            RequestCredentials();
                        }

                        Trace.TraceInformation("Error. Current Pos = " + u.CurrentPosition);
                        Trace.TraceInformation("Error was: " + e.Error.ToString() + " - " + e.Error.Message);
                        u.Error = e.Error.Message;
                        u.UploadItem.Status = e.Error.Message;
                        u.UploadItem.Status = "Tried (" + u.RetryCounter + ") - Last error was: " + u.Error;
                        u.UploadItem.UserState = u;
                        TryARetry(u);
                    }
                    else
                    {
                        ParseAndFinish(u, e.ResponseStream);
                    }
                }

                RemoveFromProcessingQueue(u);

                // only add new ones, if we did not have a cancellation
                if (!e.Cancelled)
                {

                    // now add a new row, if there is one
                    // this will go over all rows and tries to upload them
                    //foreach (UploadItem row in upload)
                    //{
                    //    //    if (UploadRow(row))
                    //    //    {
                    //    //        break;
                    //    //    }
                    //}
                }

                Trace.Unindent();
            }

            if (Finished())
            {
                var u = e.UserState as UserState;

                var  myArgs = new UploadArgs(u.UploadItem);

                OnUploadCompleted(this, myArgs);
                // Tracking number is available, raise the event.
                //OnShipmentMade(this, myArgs);

                //RequestUploadComplete();

                //  ToggleButtons(true);

                //if (this.autoStart && this.outputFileName != null)
                //{
                //    //    SaveGridAsCSV(outputFileName);
                //}
            }
            //SetTitle();
        }

        // The delegate procedure we are assigning to our object
        public delegate void UploadHandler(object sender,
                                             UploadArgs args);

        public event UploadHandler OnUploadCompleted;

        //public event EventHandler OnUploadCompleted;

        //protected void RequestUploadComplete()
        //{
        //    var handler = OnUploadCompleted;
        //    if (handler != null)
        //        handler(this, EventArgs.Empty);
        //}

        public event EventHandler OnCredentials;

        protected void RequestCredentials()
        {
            var handler = OnCredentials;
            if (handler != null)
                handler(this, EventArgs.Empty);
        }

        //private void csvDisplayGrid_CellMouseEnter(object sender, DataGridViewCellEventArgs e)
        //{
        //    if (e.RowIndex >= 0 && e.ColumnIndex >= 0)
        //    {
        //        DataGridViewCell cell = this.csvDisplayGrid.Rows[e.RowIndex].Cells[e.ColumnIndex];

        //        if (e.ColumnIndex == COLUMNINDEX_FILENAME)
        //        {
        //            string contentType = MediaFileSource.GetContentTypeForFileName(cell.Value.ToString());
        //            cell.ToolTipText = "We assume this file is of type: " + contentType;
        //        }
        //    }
        //}

        private void CancelAsync_Click(object sender, EventArgs e)
        {
            CancelProcessingQueue();

            // ToggleButtons(true);
        }

        //private void GetCmdLineArgs()
        //{
        //    string[] args = Environment.GetCommandLineArgs();

        //    foreach (string arg in args)
        //    {
        //        this.autoStart = YouTubeUploader.AutoStartString.Equals(arg.ToLower());
        //    }
        //}

        // the retry timer ticks once a minute.
        private void RetryTimer_Tick(object sender, EventArgs e)
        {
            // sometimes this message get's posted to us even 
            // though we disabled the retrytimer shortly before.
            // so better check if we are still supposed to process
            if (this._retryTimer.Enabled)
            {
                Trace.TraceInformation("Entering the retryTimer");
                RetryRows();
            }
        }

        private void LoadAppSettings()
        {
            // Get the AppSettings collection.
            NameValueCollection appSettings = ConfigurationManager.AppSettings;

            if (appSettings[YouTubeUploader.CONFIG_DEVKEY] != null)
            {
                this.developerKey = appSettings[YouTubeUploader.CONFIG_DEVKEY];
            }
            else
            {
                this.developerKey = YouTubeUploader.DeveloperKeyDefault;
            }

            if (String.IsNullOrEmpty(this.developerKey))
            {
                MessageBox.Show("You need to enter a developer key in the source code. Look for DeveloperKeyDefault and paste your key in");
                //    this.Close();
            }

            if (appSettings[YouTubeUploader.CONFIG_MAXTHREADS] != null)
            {
                MaxQueue = int.Parse(appSettings[YouTubeUploader.CONFIG_MAXTHREADS]);
            }
            else
            {
                MaxQueue = 1;
            }

            if (appSettings[YouTubeUploader.CONFIG_RETRYCOUNT] != null)
            {
                AutomaticRetries = int.Parse(appSettings[YouTubeUploader.CONFIG_RETRYCOUNT]);
            }
            else
            {
                AutomaticRetries = 3;
            }

            if (appSettings[YouTubeUploader.CONFIG_CHUNKSIZE] != null)
            {
                ChunkSize = int.Parse(appSettings[YouTubeUploader.CONFIG_CHUNKSIZE]);
            }
            else
            {
                //  this.ChunkSize.Value = 25;
            }

            // this.csvFileName = appSettings[YouTubeUploader.CONFIG_CSVFILE];
            // this.outputFileName = appSettings[YouTubeUploader.CONFIG_OUTPUTFILE];
            if (appSettings[YouTubeUploader.CONFIG_USERNAME] != null)
            {
                this._credentials = new GDataCredentials(appSettings[YouTubeUploader.CONFIG_USERNAME],
                    appSettings[YouTubeUploader.CONFIG_PASSWORD]);
            }
            this.youtubeAccount = appSettings[YouTubeUploader.CONFIG_YTACCOUNT];
        }

        private ResumableUploader ru = null;
        /// <summary>
        /// userstate is the object that is passed to the async code to identify a particular upload
        /// this object remembers the http operation, the row in the spreadsheet, progress, etc.
        /// This object get's added to the the current queue, or the retryqueue
        /// </summary>
        [Serializable]
        public class UserState
        {
            internal UserState(UploadItem r)
            {
                UploadItem = r;
                CurrentPosition = 0;
                RetryCounter = 0;
            }


            internal UploadItem UploadItem { get; private set; }

            internal long CurrentPosition { get; set; }

            internal string Error { get; set; }

            internal int RetryCounter { get; set; }

            internal string HttpVerb { get; set; }

            internal Uri ResumeUri { get; set; }
        }
        // helper to create a ResumableUploader object and setup the event handlers
        private void EnsureRU()
        {
            this.ru = new ResumableUploader(ChunkSize);
            this.ru.AsyncOperationCompleted += new AsyncOperationCompletedEventHandler(this.OnDone);
            this.ru.AsyncOperationProgress += new AsyncOperationProgressEventHandler(this.OnProgress);
        }

        // retry one row. The userstate was removed from the retry queue before that call
        private bool RetryRow(UserState us)
        {
            if (us != null)
            {
                Trace.Indent();
                Trace.TraceInformation("Retrying a row, current position is: " + us.CurrentPosition + "for uri: " + us.ResumeUri);
                Trace.Unindent();
                if (us.CurrentPosition > 0 && us.ResumeUri != null)
                {
                    var contentType = MediaFileSource.GetContentTypeForFileName(us.UploadItem.Filepath);
                    var mfs = new MediaFileSource(us.UploadItem.Filepath, contentType);

                    Stream s = mfs.GetDataStream();
                    s.Seek(us.CurrentPosition, SeekOrigin.Begin);

                    lock (this._flag)
                    {
                        this._queue.Add(us);
                    }
                    us.UploadItem.Status = "Retrying (" + us.RetryCounter + ") - Last error was: " + us.Error;
                    ru.ResumeAsync(this.youTubeAuthenticator, us.ResumeUri, us.HttpVerb, s, contentType, us);

                    return true;
                }

                // else treat this as a new one, a resume from null
                return InsertVideo(us.UploadItem, us.RetryCounter + 1);
            }
            return false;
        }

        /// <summary>
        /// insert a new video. 
        /// </summary>
        /// <param name="item"></param>
        /// <param name="retryCounter"></param>
        /// <returns></returns>
        private bool InsertVideo(UploadItem item, int retryCounter)
        {
            Trace.TraceInformation("Entering InsertVideo: starting a new upload");
            var v = new Video
                        {
                            Title = item.Title,
                            Description = item.Description,
                            Keywords = item.Keywords,
                            Private = item.Private
                        };
            v.Tags.Add(new MediaCategory(item.Tags));

            // todo : controlla questo
            var contentType = MediaFileSource.GetContentTypeForFileName(item.Filepath);
            v.MediaSource = new MediaFileSource(item.Filepath, contentType);

            // add the upload uri to it
            var link =
                new AtomLink("http://uploads.gdata.youtube.com/resumable/feeds/api/users/" + this.youtubeAccount +
                             "/uploads") { Rel = ResumableUploader.CreateMediaRelation };
            v.YouTubeEntry.Links.Add(link);

            var u = new UserState(item) { RetryCounter = retryCounter };

            lock (this._flag)
            {
                this._queue.Add(u);
            }

            ru.InsertAsync(this.youTubeAuthenticator, v.YouTubeEntry, u);

            item.Status = "Queued up...";
            item.UserState = u;

            return true;
        }

        // when an upload is completed successfully we parse the return stream
        // to setup the videoid.
        // also all retry counts will be reset to 0, as a successful upload incidates
        // a restored network condition (e.g.).
        private void ParseAndFinish(UserState u, Stream s)
        {
            var ys = new YouTubeRequestSettings(YouTubeUploader.ApplicationName,
                this.developerKey);
            var ytr = new YouTubeRequest(ys);
            var v = ytr.ParseVideo(s);

            Trace.TraceInformation("Upload successful");
            u.UploadItem.Status = "Upload successful";
            u.UploadItem.UserState = u;
            u.UploadItem.VideoId = v.VideoId;
            // we had one successful upload, reset the retry counts to 0
            lock (this._flag)
            {
                foreach (UserState us in _retryQueue)
                {
                    us.RetryCounter = 0;
                }
            }
        }

        private void RemoveFromProcessingQueue(UserState u)
        {
            lock (this._flag)
            {
                this._queue.Remove(u);
            }
        }

        private bool AddToRetryQueue(UserState u)
        {
            u.RetryCounter++;
            if (u.RetryCounter > AutomaticRetries)
            {
                u.UploadItem.Status = "Number of retries exceeded. Last error was: " + u.Error;
                return false;
            }

            lock (this._flag)
            {
                this._retryQueue.Add(u);
            }
            return true;
        }

        // moves an entry into the retry queue
        // and enables the retry timer
        private void TryARetry(UserState u)
        {
            if (AddToRetryQueue(u))
            {
                this._retryTimer.Enabled = true;
            }
        }

        // cancels all currently active threads and empties the queue
        private void CancelProcessingQueue()
        {
            // this will go over all rows and tries to upload them
            lock (this._flag)
            {
                this._retryTimer.Enabled = false;
                foreach (UserState u in this._queue)
                {
                    this.ru.CancelAsync(u);
                }
                this._queue = new List<UserState>();
            }
        }

        // walks over the retry queue and moves them into the processing queue
        // if the queue has free slots
        private void RetryRows()
        {
            if (this._retryQueue.Count > 0)
            {
                this._retryTimer.Enabled = true;
                while (this._queue.Count < (int)MaxQueue &&
                       this._retryQueue.Count > 0)
                {
                    lock (this._flag)
                    {
                        UserState u = this._retryQueue[0];
                        this._retryQueue.Remove(u);
                        RetryRow(u);
                    }
                }
            }
            else
            {
                this._retryTimer.Enabled = false;
                if (Finished())
                {
                    //ToggleButtons(true);

                }
            }
        }



        public bool CredentialsCorrect { get; set; }

        public class UploadArgs : EventArgs
        {
            private readonly UploadItem _uploadItem;

            public UploadArgs(UploadItem uploadItem)
            {
                _uploadItem = uploadItem;
            }

            public UploadItem UploadItem
            {
                get
                {
                    return _uploadItem;
                }
            }
        }

        /// <summary>
        /// Metodo per continuare uploading lasciati in sospeso.
        /// </summary>
        internal void ContinuePreviousUploading(string username, string exerciseId)
        {
            /*
             * Prova ad aprire file temporanei
             */
            var file = FilepathHelper.GetPartialUpload(username, exerciseId);
            //while ()
            //{
                
            //}
        }
    }
}
